#!/bin/bash
#
# Component that implements subscription update/checking logic
#
# Author:   Dr. Mike Murphy (mmurphy2@coastal.edu)
# Revision: 19 November 2013
#
#   Copyright 2012-2013 Coastal Carolina University
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#


# Mechanism-independent locations for storing files
declare -g PSUB_PAYLOAD_FILE="${P_STATEDIR}/subscription/payload"
declare -g PSUB_SIGNATURE_FILE="${P_STATEDIR}/subscription/signature"


## psub_bootstrap [options] [URL]
##
## Performs an initial subscription bootstrap procedure, setting the various
## subscription-related context items. Bootstrapping can be done either by
## specifying each needed piece of information as an option, or by providing
## the [URL] or path to a local bootstrap file. Returns 0 if the bootstrap
## is successful, non-zero otherwise.
##
## [options] are:
##   --subscribe <url>      Subscription URL (required)
##   --type <mechanism>     Subscription mechanism (default: twofile)
##   --security <provider>  Security provider (default: gpg)
##   --pub-key <url>        Import public key from <url>
##   --nokey                Do not import public key (overrides --pubkey)
##
function psub_bootstrap() {
	local bsfile=
	local subscribe=
	local mech=
	local provider=
	local pubkey=
	local nokey=0
	local lopts="subscribe:,type:,security:,pub-key:,nokey"
	local args=$(getopt -o "+" -l "${lopts}" -n "$0" -- "$@")
	local status=$?
	
	if ((status == 0)); then
		eval set -- "${args}"
		
		while [[ "$1" != "--" ]]; do
			case "$1" in
				--subscribe)
					shift
					subscribe="$1"
					shift
				;;
				--type)
					shift
					mech="$1"
					shift
				;;
				--security)
					shift
					provider="$1"
					shift
				;;
				--pub-key)
					shift
					pubkey="$1"
					shift
				;;
				--nokey)
					shift
					nokey=1
					# no shift: no arg
				;;
			esac
		done
		
		shift
		
		if [[ -n "$1" ]]; then
			bsfile=$(p_download_temp "$1")
			status=$?
			
			if ((status == 0)); then
				if [[ -z "${subscribe}" ]]; then
					subscribe=$(file_value "${bsfile}" "SUBSCRIBE")
				fi
				if [[ -z "${mech}" ]]; then
					mech=$(file_value "${bsfile}" "TYPE")
				fi
				if [[ -z "${provider}" ]]; then
					provider=$(file_value "${bsfile}" "SECURITY")
				fi
				if [[ -z "${pubkey}" ]]; then
					pubkey=$(file_value "${bsfile}" "PUB_KEY")
				fi
			fi
		fi
		
		# Error if no subscription is set
		if [[ -z "${subscription}" ]]; then
			echo "ERROR: No subscription URL provided in bootstrap"
			status=4
		fi
		
		# Warnings when using defaults
		if [[ -z "${mech}" ]]; then
			echo "WARNING: Bootstrap does not specify subscription mechanism"
			echo "WARNING: Defaulting to twofile"
			mech="twofile"
		fi
		
		if [[ -z "${provider}" ]]; then
			echo "WARNING: Bootstrap does not specify security provider"
			echo "WARNING: Defaulting to gpg"
			provider="gpg"
		fi
		
		if [[ -z "${pubkey}" ]]; then
			if ((nokey == 0)); then
				if [[ "${provider}" != "none" ]]; then
					echo "WARNING: Bootstrap does not specify public key URL"
					echo "WARNING: Public key will need to be imported manually"
				fi
				nokey=1
			fi
		fi
	fi
	
	if ((status == 0)); then
		# Good to go: set context
		set_context pulley/subscription "${subscription}"
		set_context pulley/subscription/mechanism "${mech}"
		set_context pulley/subscription/security "${provider}"
		status=$?
		
		# Import key, if requested
		if ((nokey == 0)); then
			psub_import_key "${pubkey}"
			status=$?
		fi
	fi
	
	return ${status}
}


## psub_get_mechanism
##
## Obtains the subscription mechanism using the pulley/subscription/mechanism
## context item (if any). If the mechanism isn't set by context, uses the
## default twofile mechanism. Returns 0.
##
function psub_get_mechanism() {
	get_context pulley/subscription/mechanism "twofile"
	return 0
}


## psub_get_provider
##
## Obtains the subscription security provider using the
## pulley/subscription/security context item (if any). If the security
## provider isn't set by context, uses the gpg provider as default.
## Returns 0.
##
function psub_get_provider() {
	get_context pulley/subscription/security "gpg"
	return 0
}


## psub_download_subscription
##
## Downloads the URL specified in the pulley/subscription context item to a
## temporary file created with s_mktempfile, and echoes the name of the
## temporary file. Returns 0 on success, 1 otherwise.
##
## Note that, in order to work correctly, the pulley/subscription context
## item must be set to a valid URL or local file path first.
##
function psub_download_subscription() {
	local sigurl=$(get_context pulley/subscription)
	local status=0
	
	if [[ -z "${sigurl}" ]]; then
		status=1
	fi
	local dl_file=$(p_download_temp "${sigurl}")
	status=$?
	
	echo "${dl_file}"
	return ${status}
}


## psub_import_key <key URL>
##
## Imports the public key for the subscription from <key URL>. Support for
## key importation may vary by security provider.
##
## Returns 0 on success, non-zero otherwise.
##
function psub_import_key() {
	local provider=$(psub_get_provider)
	psec_import_key subscription "${provider}" "$1"
	return $?
}


## psub_diff_signature <compare file>
##
## Determines if the <compare file> differs from the saved Pulley signature
## file from the prior run. If these files differ, then the subscription
## requires updating. Returns 0 if the files are the same, non-zero if the
## files differ or an error occurs.
##
function psub_diff_signature() {
	diff -q "$1" "${PSUB_SIGNATURE_FILE}" > /dev/null 2>&1
	return $?
}


## psub_verify_payload [<payload file> [signature file] [provider]]
##
## Verifies the cryptographic signature of <payload file> using the
## signature included in [signature file] and the specified security
## [provider]. If the <payload file> is omitted, verifies the cryptographic
## cryptographic signature of the existing payload using the existing
## signature and saved provider information. Returns 0 on success, non-zero
## on failure.
##
function psub_verify_payload() {
	local payload="$1"
	local sigfile="$2"
	local provider="$3"
	local status=0
	
	[[ -z "${payload}" ]] && payload="${PSUB_PAYLOAD_FILE}"
	[[ -z "${sigfile}" ]] && sigfile="${PSUB_SIGNATURE_FILE}"
	[[ -z "${provider}" ]] && provider=$(psub_get_provider)
	
	psec_verify_signature subscription "${provider}" "${sigfile}" "${payload}"
	status=$?
	
	return ${status}
}


## psub_check_update [subscription file]
##
## Checks to see if a subscription update is available. Verifies that the
## existing subscription has not become corrupt, then uses the configured
## subscription mechanism to check for updates. Returns 0 if no update is
## available, 100 if an update is available, or other non-zero value if an
## error occurs.
##
## If [subscription file] is supplied, uses that file as the new incoming
## subscription file. Otherwise, calls psub_download_subscription to
## acquire it.
##
function psub_check_update() {
	local status=100
	local mech=$(psub_get_mechanism)
	local subscription
	
	if psub_verify_payload; then
		if [[ -n "$1" ]]; then
			subscription="$1"
			status=0
		else
			subscription="${psub_download_subscription}"
			status=$?
		fi
		
		if ((status == 0)); then
			if is_function psub_${mech}_check_update; then
				psub_${mech}_check_update "${subscription}"
				status=$?
			fi
		fi
	fi
	
	return ${status}
}


## psub_update_subscription
##
## Updates the local Pulley subscription file, if psub_check_update
## indicates that an update is needed. Returns 0 on success, non-zero if
## an error occurs.
##
function psub_update_subscription() {
	local mech=$(psub_get_mechanism)
	local subscription=
	local tempdir=
	local status=1
	
	if is_function psub_${mech}_update_subscription; then
		subscription=$(psub_download_subscription)
		status=$?
		
		if ((status == 0)); then
			psub_check_update "${subscription}"
			status=$?
		fi
		
		if ((status == 100)); then
			echo "Updating Pulley subscription..."
			
			tempdir=$(s_mktempdir)
			
			# On success, subscription mechanism will leave the updated payload
			# and signature files in the specified temporary directory
			psub_${mech}_update_subscription "${tempdir}" "${subscription}"
			status=$?
			
			if ((status == 0)); then
				psub_verify_payload "${tempdir}/payload" "${tempdir}/signature"
				status=$?
				
				if ((status == 0)); then
					mv -f "${tempdir}/payload" "${PSUB_PAYLOAD_FILE}"
					mv -f "${tempdir}/signature" "${PSUB_SIGNATURE_FILE}"
					chmod 600 "${PSUB_PAYLOAD_FILE}"
					chmod 644 "${PSUB_SIGNATURE_FILE}"
					
					# Internal consistency check
					psub_verify_payload
					status=$?
				else
					echo "Signature verification failed: not updating subscription"
				fi
			else
				echo "Subscription update failed"
			fi
		elif ((status == 0)); then
			echo "Pulley subscription is up to date"
		else
			echo "Subscription configuration error: cannot update"
		fi
	else
		echo "Subscription configuration error: cannot update"
	fi
	
	return ${status}
}


## import_subscription
##
## Verifies that the local subscription against its saved signature, then
## loads the local copy of the subscription payload by sourcing it. This
## function should be called whenever a command needs information from the
## subscription to complete properly. Returns 0 on success, non-zero if an
## error occurs.
##
function import_subscription() {
	local status=1
	
	if psub_verify_payload; then
		. "${PSUB_PAYLOAD_FILE}"
		status=$?
	fi
	
	return ${status}
}
